package il.technion.cs236369.webserver;

import il.technion.cs236369.common.Common;
import il.technion.cs236369.exceptions.ParsingException;
import il.technion.cs236369.interfaces.ITypeHandler;
import il.technion.cs236369.parsers.XMLToJson;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.Date;
import java.util.Map;
import java.util.Properties;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.message.BasicHttpResponse;
import org.json.JSONObject;

/**
 * Handles the Request
 * @author gil
 *
 */
public class RequestHandler implements Runnable {
	private final Request		_request;

	/**
	 * Creates a new RequestHandler. Handles a single HTTP request and sends the
	 * response.
	 * 
	 * @param configuration The WebServer's configuration
	 * @param request The HTTP request
	 */
	public RequestHandler(Request request) {
		_request = request;
	}

	/*
	 * ####################################################### # Public
	 * Functions # #######################################################
	 */


	@Override
	public void run() {
		Common.getOut().println("Handling request! " + _request);
		//check if path is valid (exists)
		if (!isPathValid(_request.getRelativePath())) {
			System.err.println("Requested path is invalid (doesnt exist)");
			sendInvalidPathResponse("Request path is invalid (doesn't exist)");
			return;
		}
		//check if path is a directory
		if (isDirectory(_request.getRelativePath())) {
			//if its the root directory, return the welcome page
			if (isRootDirectory(_request.getRelativePath())) {
				Common.getOut().println("Requested path is the root directory (" + _request.getRelativePath() + ")");
				sendWelcome();
			} else {
				//if not the, handle directory path response
				Common.getOut().println("Requested path is a directory (" + _request.getRelativePath() + ")");
				sendForbiddenPathResponse("Requested path is a directory");
			}
			return;
		}
		//request path exists and is not a directory
		String extension = getRequestedFileExtension(_request.getRelativePath());
		TypeHandlerContainer handlerContainer = WebServerConfiguration.getInstance().getSpecificHandler(extension);
		if (handlerContainer != null) {
			handleSpecificType(handlerContainer);
			return;
		}
		//not a specific type extension
		if (_request.isPostMethod()) {
			//request is POST and does not have a specific type handler
			sendBadRequest("POST request without a specific type handler");
			return;
		}

		//check if requested file's extension is supported by the webserver
		if (WebServerConfiguration.getInstance().getMime(extension) == null) {
			sendForbiddenPathResponse("The requested resource (extension=" + extension + ") is not supported");
			return;
		}

		//check if needs a convertion from xml to json.
		Header h = _request.getRequest().getFirstHeader("Convert");
		if (h != null && h.getValue().equals("xml/json")) {
			if (!getRequestedFileExtension(_request.getAbsolutePath()).toLowerCase().equals("xml")) {
				sendBadRequest("Found a Convert: xml/json header while requested resource is not an XML file");
				return;
			}
			handleXMLtoJSON();
			return;
		}
		//no special attention needed
		HttpEntity entity = new FileEntity(getRequestedFile(_request.getRelativePath()), WebServerConfiguration.getInstance().getMime(extension));
		HttpResponse response = generateResponse(200, "OK", entity);
		sendResponseAndClose(_request.getSocket(), _request.getConnection(), response);
	}

	/*
	 * ####################################################### # Static
	 * Functions # #######################################################
	 */

	/**
	 * Generates an error HTTP response. The response contains the given error
	 * indicators. The response also contains an HTML page that displays the
	 * error and a short description.
	 * 
	 * @param errorCode The response's status code from RFC 2616.
	 * @param errorMsg The response's reason-phrase from RFC 2616.
	 * @param description The description to be displayed within the HTML page.
	 *            Can use <br>
	 *            for multiple lines.
	 * @return Return an HTTP response containing the given information and an
	 *         error HTML page entity.
	 */
	public static HttpResponse generateErrorResponse(int errorCode, String errorMsg, String description) {
		StringEntity entity = null;
		//create the HTML error page
		try {
			entity = new StringEntity("<html><head><title>" + errorCode + " Error Page</title></head><body><h1>" + errorCode + " " + errorMsg
					+ "</h1><p>" + description + "</p></body></html>");
			entity.setContentType(WebServerConfiguration.getInstance().getMime("html"));
		} catch (UnsupportedEncodingException e) {
			System.err.println("Unable to create string entity for the error page");
		}
		//create the response
		HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, errorCode, errorMsg);
		response.setEntity(entity);
		//set headers
		Common.getOut().println("Generating headers:");
		setGeneralHeaders(response);
		//		//change content-type
		//		response.setHeader("Content-Type",entity.getContentType().getValue());
		return response;
	}

	/**
	 * Generates an HTTP response with given code and reason-phrase.
	 * Inserting the given HTTP entity to the response.
	 * @param code The response's code.
	 * @param msg The response's reason-phrase.
	 * @param entity The entity to insert into the response.
	 * @return The generated HTTP response.
	 */
	public static HttpResponse generateResponse(int code, String msg, HttpEntity entity) {
		HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, code, msg);
		response.setEntity(entity);
		Common.getOut().println("Generating headers:");
		//set headers
		setGeneralHeaders(response);
		//		//content-type
		//		setContentTypeHeaderByRequest(response);
		return response;
	}

	/**
	 * Sends the response and close the connections
	 * 
	 * @param response The response
	 */
	public static void sendResponseAndClose(Socket socket, DefaultHttpServerConnection conn, HttpResponse response) {
		//send response
		Common.getOut().println("Out going connection to " + socket.getInetAddress());
		try {
			conn.sendResponseHeader(response);
			conn.sendResponseEntity(response);
			conn.flush();
		} catch (HttpException | IOException | IllegalStateException e) {
			System.err.println("Unable to send response");
		} finally {
			try {
				conn.close();
				//socket.close();
			} catch (IOException e) {
				System.err.println("Unable to close connection");
			}
		}
	}

	/*
	 * ####################################################### # Private Static
	 * Functions # #######################################################
	 */

	private static void setGeneralHeaders(HttpResponse response) {
		//time
		setTimeHeader(response);
		//content-length
		setContentLengthHeader(response);
		//connection
		setConnectionCloseHeader(response);
		//content-type
		setContentType(response);
	}

	private static void setContentType(HttpResponse response) {
		//set response's content-type to entity's content-type
		String type = response.getEntity().getContentType().getValue();
		Common.getOut().println("header: Content-Type=" + type);
		response.setHeader("Content-Type", type);

	}

	private static void setConnectionCloseHeader(HttpResponse response) {
		//set response's connection to close
		Common.getOut().println("header: Connection=close");
		response.setHeader("Connection", "close");
	}

	private static void setContentLengthHeader(HttpResponse response) {
		//set response's conent-length to entity's length
		HttpEntity entity = response.getEntity();
		long entitySize = 0;
		if (entity != null) {
			entitySize = entity.getContentLength();
		}
		Common.getOut().println("header: Content-Length=" + entitySize);
		response.setHeader("Content-Length", "" + entitySize);
	}

	private static void setTimeHeader(HttpResponse response) {
		//set response's date to current time
		String time = DateTool.rfc1123Format.format(new Date(System.currentTimeMillis()));
		Common.getOut().println("header: Date=" + time);
		response.setHeader("Date", time);
	}

	/*
	 * ####################################################### # Private
	 * Functions # #######################################################
	 */

	private void handleXMLtoJSON() {
		//handle request for xml file with convertion to json
		Common.getOut().println("Converting XML to JSON");
		XMLToJson parser = new XMLToJson();
		File xmlFile = getRequestedFile(_request.getRelativePath());
		JSONObject json = null;
		//parse the xml
		try {
			json = parser.parse(xmlFile);
		} catch (ParsingException e) {
			sendBadRequest("The requested XML cannot be converted to JSON");
			return;
		} catch (FileNotFoundException e) {
			sendInvalidPathResponse("The requested XML cannot be found");
			return;
		}
		//create an entity with the json context
		StringEntity entity = null;
		try {
			entity = new StringEntity(json.toString());
			entity.setContentType(WebServerConfiguration.getInstance().getMime("json"));
		} catch (UnsupportedEncodingException e) {
			sendInternalErrorResponse("Unsupported Encoding during JSON handling");
		}
		//generate the response
		HttpResponse response = generateResponse(200, "OK", entity);
		//		Common.getOut().println("Changing headers:");
		//		//change content-type to json
		//		response.setHeader("content-type",entity.getContentType().getValue());
		sendResponseAndClose(_request.getSocket(), _request.getConnection(), response);

	}

	private void handleSpecificType(TypeHandlerContainer handlerContainer) {
		//handle request for a specific type which has a specific handler
		ITypeHandler handler = null;
		//try to create an instance of the specific handler

		try {
			//get the ctor
			Constructor<?> ctor = handlerContainer.getHandlerClass().getConstructor(Properties.class);
			Properties props = new Properties();
			//add each parameters that was predefined in the configuration
			Map<String, String> parameters = handlerContainer.getParameters();
			for (String key : parameters.keySet()) {
				Common.getOut().println("param name="+key+", value="+parameters.get(key));
				props.setProperty(key, parameters.get(key));
			}
			//add the session parameter
			//			props.put("session", new SessionImpl());
			handler = (ITypeHandler)ctor.newInstance(props);
//			handler = handlerContainer.getHandler();
		} catch (NoSuchMethodException e) {
			System.err.println("Can't create the specific handler for the request: Can't get handler's constructor! ");
			sendInternalErrorResponse("Can't create the specific handler for the request:"
					+ "<br>NoSuchMethodException - Can't get handler's constructor!");
			return;
		} catch (InstantiationException e) {
			System.err.println("Can't create the specific handler for the request: Can't create handler's instance.");
			sendInternalErrorResponse("Can't create the specific handler for the request:"
					+ "<br>InstantiationException - Can't create handler's instance.");
			return;
		} catch (IllegalAccessException e) {
			System.err.println("Can't create the specific handler for the request: Illegal access!");
			sendInternalErrorResponse("Can't create the specific handler for the request:" + "<br>IllegalAccessException - Illegal access!");
			return;
		} catch (IllegalArgumentException e) {
			System.err.println("Can't create the specific handler for the request: Illegal arguments!");
			sendInternalErrorResponse("Can't create the specific handler for the request:" + "<br>IllegalArgumentException - Illegal arguments!");
			return;
		} catch (InvocationTargetException e) {
			System.err.println("Can't create the specific handler for the request: Cannot invoke handler.");
			sendInternalErrorResponse("Can't create the specific handler for the request:" + "<br>InvocationTargetException - Cannot invoke handler.");
			return;
		}
		assert (handler != null);

		//handle the request
		HttpResponse response = handler.handle(_request);
		//send the response
		sendResponseAndClose(_request.getSocket(), _request.getConnection(), response);
	}

	private void sendWelcome() {
		//send the welcome page as a response
		HttpEntity entity = new FileEntity(WebServerConfiguration.getInstance().getWelcomeFile(), WebServerConfiguration.getInstance().getMime(
				getRequestedFileExtension(WebServerConfiguration.getInstance().getWelcomeFile().getName())));
		HttpResponse response = generateResponse(200, "OK", entity);
		sendResponseAndClose(_request.getSocket(), _request.getConnection(), response);
	}

	private void sendBadRequest(String desc) {
		//send bad request response
		HttpResponse response = generateErrorResponse(400, "Bad Request", desc);
		sendResponseAndClose(_request.getSocket(), _request.getConnection(), response);
	}

	private void sendInternalErrorResponse(String desc) {
		//send internal error response
		HttpResponse response = generateErrorResponse(500, "Internal Server Error", desc);
		sendResponseAndClose(_request.getSocket(), _request.getConnection(), response);
	}

	private void sendForbiddenPathResponse(String desc) {
		//send directory response
		HttpResponse response = generateErrorResponse(403, "Forbidden", desc);
		sendResponseAndClose(_request.getSocket(), _request.getConnection(), response);
	}

	private void sendInvalidPathResponse(String desc) {
		//send invalid path response
		HttpResponse response = generateErrorResponse(404, "Not Found", desc);
		sendResponseAndClose(_request.getSocket(), _request.getConnection(), response);
	}

	private String getRequestedFileExtension(String absoluteUrl) {
		//return the file extension
		int startIndex = absoluteUrl.lastIndexOf(".");
		return absoluteUrl.substring(startIndex + 1);
	}

	private File getRequestedFile(String relativeUrl) {
		//return the requested file
		File f = new File(WebServerConfiguration.getInstance().getBaseDirName(), relativeUrl);
		assert (f.canRead() == true);
		return f;
	}

	private boolean isDirectory(String relativeUrl) {
		File f = new File(WebServerConfiguration.getInstance().getBaseDirName(), relativeUrl);
		//check if given path is a directory
		if (f.isDirectory()) {
			return true;
		}
		return false;
	}

	private boolean isPathValid(String relativeUrl) {
		File base = new File(WebServerConfiguration.getInstance().getBaseDirName());
		File file = new File(WebServerConfiguration.getInstance().getBaseDirName(), relativeUrl);
		String fileCanonicalPath = "";
		String baseCanonicalPath = "";
		try {
			fileCanonicalPath = file.getCanonicalPath();
			baseCanonicalPath = base.getCanonicalPath();
		} catch (IOException e1) {
			System.err.println("Can't retrieve canonical path");
			return false;
		}
		Common.getOut().println("Requested canonical path: "+fileCanonicalPath);
		Common.getOut().println("Base canonical path: "+baseCanonicalPath);
		//check if given path exists and readable
		if (!file.exists() || !file.canRead()) {
			return false;
		}
		//verify that the relative path is under the base folder's path
		if (fileCanonicalPath.startsWith(baseCanonicalPath)) {
			return true;
		}
		System.err.println("Requested path is not within the server's base folder");
		return false;
	}
	
	private boolean isRootDirectory(String relativePath) {
		File file = new File(WebServerConfiguration.getInstance().getBaseDirName(), relativePath);
		File base = new File(WebServerConfiguration.getInstance().getBaseDirName());
		String fileCanonicalPath = "";
		String baseCanonicalPath = "";
		try {
			fileCanonicalPath = file.getCanonicalPath();
			baseCanonicalPath = base.getCanonicalPath();
		} catch (IOException e) {
			System.err.println("Can't retrieve canonical path");
			return false;
		}
		if (fileCanonicalPath.replace("\\","").equals(baseCanonicalPath.replace("\\",""))) {
			return true;
		}
		return false;
	}
}
